require "engine.class"
require "engine.Actor"
require "engine.Autolevel"
require "engine.interface.ActorInventory"
require "engine.interface.ActorTemporaryEffects"
require "engine.interface.ActorLife"
require "engine.interface.ActorProject"
require "engine.interface.ActorLevel"
require "engine.interface.ActorStats"
require "engine.interface.ActorTalents"
require "engine.interface.ActorResource"
require "engine.interface.ActorQuest"
require "engine.interface.BloodyDeath"
require "engine.interface.ActorFOV"
require "mod.class.interface.Combat"
local Faction = require "engine.Faction"
local Map = require "engine.Map"
local DamageType = require "engine.DamageType"

module(..., package.seeall, class.inherit(
	engine.Actor,
	engine.interface.ActorInventory,
	engine.interface.ActorTemporaryEffects,
	engine.interface.ActorLife,
	engine.interface.ActorProject,
	engine.interface.ActorLevel,
	engine.interface.ActorStats,
	engine.interface.ActorTalents,
	engine.interface.ActorResource,
	engine.interface.ActorQuest,
	engine.interface.BloodyDeath,
	engine.interface.ActorFOV,
	mod.class.interface.Combat
))

-- Dont save the can_see_cache
--_M._no_save_fields.can_see_cache = true

function _M:init(t, no_default)
	-- Define some basic combat stats
	self.combat_def = 0
	self.combat_armor = 0
	self.combat_atk = 0
	self.combat_apr = 0
	self.combat_dam = 0
	self.combat_bonus = 0
	
	self.perm = self.perm or false
	self.no_blow = self.no_blow or false
	self.invulnerable = self.invulnerable or false
	self.life_rating = self.life_rating or 10

	-- Default regen
	t.life_regen = t.life_regen or 0.05 -- Life regen handled in rest code

	t.resists = t.resists or {}
	
	-- Default melee barehanded damage
	self.combat = { dam=1, atk=1, apr=0, dammod={str=1} }

	engine.Actor.init(self, t, no_default)
	engine.interface.ActorInventory.init(self, t)
	engine.interface.ActorTemporaryEffects.init(self, t)
	engine.interface.ActorLife.init(self, t)
	engine.interface.ActorProject.init(self, t)
	engine.interface.ActorTalents.init(self, t)
	engine.interface.ActorResource.init(self, t)
	engine.interface.ActorStats.init(self, t)
	engine.interface.ActorLevel.init(self, t)
	engine.interface.ActorFOV.init(self, t)

--	self:resetCanSeeCache()
end

function _M:change_color(r, g, b)
	self.color_r = r
	self.color_g = g
	self.color_b = b
	if self._mo then self._mo:invalidate() end
	game.level.map:updateMap(self.x, self.y)
end

function _M:act()
	if not engine.Actor.act(self) then return end
	
	if self == game.player then
		self:change_color(255, self.life/self.max_life * 255, self.life/self.max_life * 255)
	elseif self.name == "earth elemental" then
		self:change_color(math.random(130,150), math.random(60,80), math.random(10))
	elseif self.name == "fire elemental" then
		self:change_color(math.random(200,255), math.random(10), math.random(50))
	elseif self.name == "air elemental" then
		self:change_color(math.random(230,255), math.random(230,255), math.random(10))
	elseif self.name == "water elemental" then
		self:change_color(math.random(10), math.random(10), math.random(200,255))
	elseif self.name == "mana elemental" then
		self:change_color(math.random(210,230), math.random(100,120), math.random(205,225))
	elseif self.name == "chaos elemental" then
		self:change_color(math.random(255), math.random(255), math.random(255))
	end

	self.changed = true

	-- Cooldown talents
	self:cooldownTalents()
	-- Compute timed effects
	self:timedEffects()

	-- Still enough energy to act ?
	if self.energy.value < game.energy_to_act then return false end

	-- Still not dead ?
	if self.dead then return false end

	-- Ok reset the seen cache
--	self:resetCanSeeCache()

	return true
end

function _M:move(x, y, force)
	local moved = false
	if force or self:enoughEnergy() then
		-- Never move but tries to attack ? ok
		if not force and self:attr("never_move") then
			-- A bit weird, but this simple asks the collision code to detect an attack
			if not game.level.map:checkAllEntities(x, y, "block_move", self, true) then
				game.logPlayer(self, "You are unable to move!")
			end
		else
			if self.x and self.y and (self.name == "fire elemental" or self.faction == "fire") then game.level.map:particleEmitter(self.x, self.y, 1, "firetrail") end
			moved = engine.Actor.move(self, x, y, force)
		end
		if not force and moved and not self.did_energy then self:useEnergy() end
	end
	self.did_energy = nil

	self:regenLife()
	return moved
end

--- Get the "path string" for this actor
--[[ See Map:addPathString() for more info
function _M:getPathString()
	local ps = self.open_door and "return {open_door=true,can_pass={" or "return {can_pass={"
	for what, check in pairs(self.can_pass) do
		ps = ps .. what.."="..check..","
	end
	ps = ps.."}}"
	print("[PATH STRING] for", self.name, " :=: ", ps)
	return ps
end]]

function _M:DEC_HEX(IN)
    local B,K,OUT,I,D=16,"0123456789ABCDEF","",0
	if IN == 0 then return "00" end
    while IN>0 do
        I=I+1
        IN,D=math.floor(IN/B),math.mod(IN,B)+1
        OUT=string.sub(K,D,D)..OUT
    end
	if string.len(OUT) == 1 then OUT = "0" .. OUT end	--make sure length is 2, 0 length taken care of
    return OUT
end

function _M:tooltip()
	local mycolor = "#" .. (self:DEC_HEX(self.color_r) or "FF") .. (self:DEC_HEX(self.color_g) or "FF") .. (self:DEC_HEX(self.color_b) or "FF") .. "#"
	local factcolor, factstate = "#ANTIQUE_WHITE#", "neutral"
	if self:reactionToward(game.player) < 0 then factcolor, factstate = "#LIGHT_RED#", "hostile"
	elseif self:reactionToward(game.player) > 0 then factcolor, factstate = "#LIGHT_GREEN#", "friendly"
	end

	local resists = {}
	for t, v in pairs(self.resists) do
		resists[#resists+1] = string.format("%d%% %s", v, t == "all" and "all" or DamageType:get(t).name)
	end

	return ([[%s%s%s
#00ffff#Level: %d
Exp: %d/%d
#ff0000#HP: %d (%d%%)
Stats: %d / %d / %d
Resists: %s
%s
Faction: %s%s (%s)]]):format(
	self:getDisplayString(), mycolor, self.name,
	self.level,
	self.exp, self:getExpChart(self.level+1) or "---",
	self.life, self.life * 100 / self.max_life,
	self:getStr(),
	self:getDex(),
	self:getCon(),
	table.concat(resists, ','),
	self.desc or "",
	factcolor, Faction.factions[self.faction].name, factstate)
end

function _M:onTakeHit(value, src)
	return value
end

function _M:die(src)
	engine.interface.ActorLife.die(self, src)

	-- Gives the killer some exp for the kill
	if src and src.gainExp then
		src:gainExp(self:worthExp(src))
	end

	-- Drop stuff
	if not self.keep_inven_on_death then
		if not self.no_drops then
			for inven_id, inven in pairs(self.inven) do
				for i, o in ipairs(inven) do
					if not o.no_drop then
						o.droppedBy = self.name
						game.level.map:addObject(self.x, self.y, o)
					end
				end
			end
		end
		self.inven = {}
	end

	return true
end

function _M:levelup()
	self.max_life = self.max_life + (2 * self.life_rating * self.STAT_CON / 100)

	self:incMaxPower(3)

	-- increase (or do random stuff with) resists
	if self.faction == "air" or self.name == "air elemental" then
		self.resists[DamageType.EARTH] = (self.resists[DamageType.EARTH] or 0) + 1
		self.resists[DamageType.LIGHTNING] = (self.resists[DamageType.LIGHTNING] or 0) + 2
	elseif self.faction == "earth" or self.name == "earth elemental" then
		self.resists[DamageType.LIGHTNING] = (self.resists[DamageType.LIGHTNING] or 0) + 1
		self.resists[DamageType.EARTH] = (self.resists[DamageType.EARTH] or 0) + 2
	elseif self.faction == "fire" or self.name == "fire elemental" then
		self.resists[DamageType.FIRE] = (self.resists[DamageType.FIRE] or 0) + 2
		self.resists[DamageType.COLD] = (self.resists[DamageType.COLD] or 0) + 1
	elseif self.faction == "water" or self.name == "water elemental" then
		self.resists[DamageType.COLD] = (self.resists[DamageType.COLD] or 0) + 2
		self.resists[DamageType.FIRE] = (self.resists[DamageType.FIRE] or 0) + 1
	elseif self.faction == "mana" or self.name == "mana elemental" then
		self.resists[DamageType.COLD] = (self.resists[DamageType.COLD] or 0) + math.random(2)
		self.resists[DamageType.FIRE] = (self.resists[DamageType.FIRE] or 0) + math.random(2)
		self.resists[DamageType.LIGHTNING] = (self.resists[DamageType.LIGHTNING] or 0) + math.random(2)
		self.resists[DamageType.EARTH] = (self.resists[DamageType.EARTH] or 0) + math.random(2)
		self.resists[DamageType.MANA] = (self.resists[DamageType.MANA] or 0) + 2
	elseif self.faction == "chaos" or self.name == "chaos elemental" then
		self.resists[DamageType.COLD] = (self.resists[DamageType.COLD] or 0) + rng.range(-1,1)
		self.resists[DamageType.FIRE] = (self.resists[DamageType.FIRE] or 0) + rng.range(-1,1)
		self.resists[DamageType.LIGHTNING] = (self.resists[DamageType.LIGHTNING] or 0) + rng.range(-1,1)
		self.resists[DamageType.EARTH] = (self.resists[DamageType.EARTH] or 0) + rng.range(-1,1)
		self.resists[DamageType.MANA] = (self.resists[DamageType.MANA] or 0) + rng.range(-1,1)
		self.resists[DamageType.CHAOS] = (self.resists[DamageType.CHAOS] or 0) + rng.range(-1,1)
	end

	-- because I am nice, and to keep the combat_bonus somewhat close ;)
	if self == game.player then self.combat_bonus = self.combat_bonus + 0.5
	else self.combat_bonus = self.combat_bonus + 0.75 end
	
	-- Heal up on new level
	self.life = self.max_life
end

--- Notifies a change of stat value
function _M:onStatChange(stat, v)
	if stat == self.STAT_CON then
		self.max_life = self.max_life + 2
	end
end

function _M:attack(target)
	self:bumpInto(target)
end


--- Called before a talent is used
-- Check the actor can cast it
-- @param ab the talent (not the id, the table)
-- @return true to continue, false to stop
function _M:preUseTalent(ab, silent)
	if not self:enoughEnergy() then print("fail energy") return false end

	if ab.mode == "sustained" then
		if ab.sustain_power and self.max_power < ab.sustain_power and not self:isTalentActive(ab.id) then
			game.logPlayer(self, "You do not have enough power to activate %s.", ab.name)
			return false
		end
	else
		if ab.power and self:getPower() < ab.power then
			game.logPlayer(self, "You do not have enough power to cast %s.", ab.name)
			return false
		end
	end

	if not silent then
		-- Allow for silent talents
		if ab.message ~= nil then
			if ab.message then
				game.logSeen(self, "%s", self:useTalentMessage(ab))
			end
		elseif ab.mode == "sustained" and not self:isTalentActive(ab.id) then
			game.logSeen(self, "%s activates %s.", self.name:capitalize(), ab.name)
		elseif ab.mode == "sustained" and self:isTalentActive(ab.id) then
			game.logSeen(self, "%s deactivates %s.", self.name:capitalize(), ab.name)
		else
			game.logSeen(self, "%s uses %s.", self.name:capitalize(), ab.name)
		end
	end
	return true
end

--- Called before a talent is used
-- Check if it must use a turn, mana, stamina, ...
-- @param ab the talent (not the id, the table)
-- @param ret the return of the talent action
-- @return true to continue, false to stop
function _M:postUseTalent(ab, ret)
	if not ret then return end

	self:useEnergy()

	if ab.mode == "sustained" then
		if not self:isTalentActive(ab.id) then
			if ab.sustain_power then
				self.max_power = self.max_power - ab.sustain_power
			end
		else
			if ab.sustain_power then
				self.max_power = self.max_power + ab.sustain_power
			end
		end
	else
		if ab.power then
			self:incPower(-ab.power)
		end
	end

	return true
end

--- Return the full description of a talent
-- You may overload it to add more data (like power usage, ...)
function _M:getTalentFullDescription(t)
	local d = {}

	if t.mode == "passive" then d[#d+1] = "#6fff83#Use mode: #00FF00#Passive"
	elseif t.mode == "sustained" then d[#d+1] = "#6fff83#Use mode: #00FF00#Sustained"
	else d[#d+1] = "#6fff83#Use mode: #00FF00#Activated"
	end

	if t.power or t.sustain_power then d[#d+1] = "#6fff83#Power cost: #7fffd4#"..(t.power or t.sustain_power) end
	if self:getTalentRange(t) > 1 then d[#d+1] = "#6fff83#Range: #FFFFFF#"..self:getTalentRange(t)
	else d[#d+1] = "#6fff83#Range: #FFFFFF#melee/personal"
	end
	if t.cooldown then d[#d+1] = "#6fff83#Cooldown: #FFFFFF#"..t.cooldown end

	return table.concat(d, "\n").."\n#6fff83#Description: #FFFFFF#"..t.info(self, t)
end

--- How much experience is this actor worth
-- @param target to whom is the exp rewarded
-- @return the experience rewarded
function _M:worthExp(target)
	if not target.level or self.level < target.level - 3 then return 0 end

	local mult = 2
	if self.unique then mult = 6
	elseif self.egoed then mult = 3 end
	return self.level * mult * self.exp_worth
end

--- Can the actor see the target actor
-- This does not check LOS or such, only the actual ability to see it.<br/>
-- Check for telepathy, invisibility, stealth, ...
function _M:canSee(actor, def, def_pct)
	if not actor then return false, 0 end

	-- Check for stealth. Checks against the target cunning and level
	if actor:attr("stealth") and actor ~= self then
		local def = self.level / 2 + self:getCun(25)
		local hit, chance = self:checkHit(def, actor:attr("stealth") + (actor:attr("inc_stealth") or 0), 0, 100)
		if not hit then
			return false, chance
		end
	end

	if def ~= nil then
		return def, def_pct
	else
		return true, 100
	end
end

--- Can the target be applied some effects
-- @param what a string describing what is being tried
function _M:canBe(what)
	if what == "poison" and rng.percent(100 * (self:attr("poison_immune") or 0)) then return false end
	if what == "cut" and rng.percent(100 * (self:attr("cut_immune") or 0)) then return false end
	if what == "confusion" and rng.percent(100 * (self:attr("confusion_immune") or 0)) then return false end
	if what == "blind" and rng.percent(100 * (self:attr("blind_immune") or 0)) then return false end
	if what == "stun" and rng.percent(100 * (self:attr("stun_immune") or 0)) then return false end
	if what == "fear" and rng.percent(100 * (self:attr("fear_immune") or 0)) then return false end
	if what == "knockback" and rng.percent(100 * (self:attr("knockback_immune") or 0)) then return false end
	if what == "instakill" and rng.percent(100 * (self:attr("instakill_immune") or 0)) then return false end
	return true
end
